#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Git 仓库迁移工具
实现从源Git仓库到目标GitLab仓库的完整迁移
"""

import os
import sys
import json
import shutil
import subprocess
import logging
from pathlib import Path
from typing import List, Dict, Optional
import urllib.parse


class GitMigrator:
    """Git仓库迁移器"""
    
    def __init__(self, config_path: str = "config.json"):
        self.config_path = config_path
        self.setup_logging()
        self.config = self.load_config()
        
    def setup_logging(self):
        """设置日志"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('migration.log', encoding='utf-8'),
                logging.StreamHandler(sys.stdout)
            ]
        )
        self.logger = logging.getLogger(__name__)
        
    def load_config(self) -> Dict:
        """加载配置文件"""
        try:
            with open(self.config_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except FileNotFoundError:
            self.logger.error(f"配置文件 {self.config_path} 不存在")
            return {}
        except json.JSONDecodeError:
            self.logger.error(f"配置文件 {self.config_path} 格式错误")
            return {}
            
    def save_config(self, config: Dict):
        """保存配置文件"""
        try:
            with open(self.config_path, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
            self.logger.info(f"配置文件已保存到 {self.config_path}")
        except Exception as e:
            self.logger.error(f"保存配置文件失败: {e}")
            
    def validate_config(self) -> bool:
        """验证配置文件的完整性"""
        required_fields = [
            'source_repo', 'source_username', 'source_password',
            'target_gitlab_url', 'target_access_token', 'target_project_path',
            'local_temp_path', 'source_project_paths'
        ]
        
        for field in required_fields:
            if field not in self.config or not self.config[field]:
                self.logger.error(f"配置文件中缺少必要字段: {field}")
                return False
                
        return True
        
    def clone_source_repo(self, project_path: str) -> Optional[str]:
        """克隆源仓库的指定项目"""
        try:
            # 构建克隆URL
            source_url = self.config['source_repo']
            if not source_url.endswith('/'):
                source_url += '/'
                
            clone_url = f"{source_url}{project_path}.git"
            
            # 准备本地临时目录路径（使用绝对路径避免嵌套问题）
            abs_temp_path = os.path.abspath(self.config['local_temp_path'])
            local_path = os.path.join(abs_temp_path, project_path.replace('/', '_'))
            
            # 确保目标目录不存在（git clone --mirror 需要空目录）
            if os.path.exists(local_path):
                shutil.rmtree(local_path)
            
            # 确保临时目录存在
            os.makedirs(abs_temp_path, exist_ok=True)
            
            # 克隆仓库
            self.logger.info(f"正在克隆项目: {project_path}")
            
            # 如果提供了用户名密码，使用认证URL
            if self.config.get('source_username') and self.config.get('source_password'):
                parsed_url = urllib.parse.urlparse(clone_url)
                # 对用户名和密码进行URL编码
                encoded_username = urllib.parse.quote(self.config['source_username'], safe='')
                encoded_password = urllib.parse.quote(self.config['source_password'], safe='')
                auth_url = f"{parsed_url.scheme}://{encoded_username}:{encoded_password}@{parsed_url.netloc}{parsed_url.path}"
                clone_cmd = ['git', 'clone', '--mirror', auth_url, local_path]
            else:
                clone_cmd = ['git', 'clone', '--mirror', clone_url, local_path]
            
            # 执行克隆命令
            self.logger.info(f"执行克隆命令: {' '.join(clone_cmd)}")
            # 使用绝对路径，避免相对路径问题
            abs_local_temp_path = os.path.abspath(self.config['local_temp_path'])
            result = subprocess.run(clone_cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', cwd=abs_local_temp_path)
            
            if result.returncode != 0:
                self.logger.error(f"克隆失败: {result.stderr}")
                return None
                
            # 验证克隆是否成功（镜像克隆创建的是裸仓库）
            if not os.path.exists(local_path) or not os.listdir(local_path):
                self.logger.error(f"克隆后的目录 {local_path} 为空或不存在")
                return None
            
            # 检查并获取LFS对象
            try:
                lfs_check = subprocess.run(['git', 'lfs', 'version'], cwd=local_path, 
                                         capture_output=True, text=True, encoding='utf-8', errors='ignore')
                if lfs_check.returncode == 0:
                    self.logger.info("检测到Git LFS，正在获取LFS对象...")
                    lfs_fetch = subprocess.run(['git', 'lfs', 'fetch', '--all'], 
                                            cwd=local_path, capture_output=True, text=True, 
                                            encoding='utf-8', errors='ignore')
                    if lfs_fetch.returncode == 0:
                        self.logger.info("LFS对象获取成功")
                    else:
                        self.logger.warning(f"LFS对象获取警告: {lfs_fetch.stderr}")
                        # 尝试跳过LFS检查
                        self.logger.info("尝试跳过LFS检查，继续迁移...")
                        try:
                            # 先清理LFS引用
                            subprocess.run(['git', 'lfs', 'untrack', '*'], cwd=local_path, 
                                         capture_output=True, text=True, encoding='utf-8', errors='ignore')
                            # 移除.gitattributes中的LFS配置
                            gitattributes_path = os.path.join(local_path, '.gitattributes')
                            if os.path.exists(gitattributes_path):
                                try:
                                    with open(gitattributes_path, 'r', encoding='utf-8') as f:
                                        content = f.read()
                                    # 移除所有LFS相关行
                                    lines = [line for line in content.split('\n') if 'filter=lfs' not in line and 'diff=lfs' not in line]
                                    with open(gitattributes_path, 'w', encoding='utf-8') as f:
                                        f.write('\n'.join(lines))
                                    self.logger.info("已清理.gitattributes中的LFS配置")
                                except Exception as attr_error:
                                    self.logger.warning(f"清理.gitattributes失败: {attr_error}")
                            
                            # 使用git filter-branch清理Git历史中的LFS引用
                            self.logger.info("正在清理Git历史中的LFS引用...")
                            try:
                                # 创建临时脚本来清理LFS引用
                                filter_script = os.path.join(local_path, 'filter_lfs.sh')
                                with open(filter_script, 'w', encoding='utf-8') as f:
                                    f.write('''#!/bin/bash
# 清理LFS引用的脚本
git filter-branch --force --index-filter '
    git rm --cached --ignore-unmatch -r . || true
    git add . || true
' --prune-empty --tag-name-filter cat -- --all
''')
                                
                                # 执行清理脚本
                                subprocess.run(['bash', filter_script], cwd=local_path, 
                                             capture_output=True, text=True, encoding='utf-8', errors='ignore')
                                self.logger.info("已清理Git历史中的LFS引用")
                                
                                # 清理临时文件
                                os.remove(filter_script)
                            except Exception as filter_error:
                                self.logger.warning(f"清理Git历史失败: {filter_error}")
                            
                            # 禁用LFS
                            subprocess.run(['git', 'lfs', 'uninstall'], cwd=local_path, 
                                         capture_output=True, text=True, encoding='utf-8', errors='ignore')
                            self.logger.info("已禁用LFS检查")
                        except Exception as lfs_error:
                            self.logger.warning(f"禁用LFS检查失败: {lfs_error}")
            except Exception as e:
                self.logger.warning(f"LFS处理过程中出现警告: {e}")
                
            self.logger.info(f"项目 {project_path} 克隆成功到 {local_path}")
            return local_path
            
        except Exception as e:
            self.logger.error(f"克隆项目 {project_path} 时发生错误: {e}")
            return None
            
    def setup_target_remote(self, local_path: str, project_path: str):
        """设置目标GitLab远程仓库"""
        try:
            # 构建目标GitLab URL
            target_url = self.config['target_gitlab_url']
            if not target_url.endswith('/'):
                target_url += '/'
                
            target_project_url = f"{target_url}{self.config['target_project_path']}/{project_path}.git"
            
            # 移除现有的远程仓库
            subprocess.run(['git', 'remote', 'remove', 'origin'], cwd=local_path, capture_output=True, text=True, encoding='utf-8', errors='ignore')
            
            # 添加新的远程仓库
            remote_cmd = [
                'git', 'remote', 'add', 'origin', target_project_url
            ]
            
            # 如果提供了access token，使用它
            if self.config.get('target_access_token'):
                parsed_url = urllib.parse.urlparse(target_project_url)
                # 对访问令牌进行URL编码
                encoded_token = urllib.parse.quote(self.config['target_access_token'], safe='')
                auth_url = f"{parsed_url.scheme}://oauth2:{encoded_token}@{parsed_url.netloc}{parsed_url.path}"
                remote_cmd = ['git', 'remote', 'add', 'origin', auth_url]
                
            result = subprocess.run(remote_cmd, cwd=local_path, capture_output=True, text=True, encoding='utf-8', errors='ignore')
            
            if result.returncode != 0:
                self.logger.error(f"设置远程仓库失败: {result.stderr}")
                return False
                
            self.logger.info(f"目标远程仓库设置成功: {target_project_url}")
            return True
            
        except Exception as e:
            self.logger.error(f"设置目标远程仓库时发生错误: {e}")
            return False
            
    def push_to_target(self, local_path: str, project_path: str) -> bool:
        """推送到目标GitLab仓库"""
        try:
            self.logger.info(f"正在推送项目 {project_path} 到目标仓库")
            
            # 检查是否启用了Git LFS
            lfs_check = subprocess.run(['git', 'lfs', 'version'], cwd=local_path, 
                                     capture_output=True, text=True, encoding='utf-8', errors='ignore')
            
            if lfs_check.returncode == 0:
                self.logger.info("检测到Git LFS，正在推送LFS对象...")
                # 先推送LFS对象
                lfs_push = subprocess.run(['git', 'lfs', 'push', '--all', 'origin'], 
                                        cwd=local_path, capture_output=True, text=True, 
                                        encoding='utf-8', errors='ignore')
                if lfs_push.returncode != 0:
                    self.logger.warning(f"LFS推送警告: {lfs_push.stderr}")
                    # 尝试禁用LFS后推送
                    self.logger.info("尝试禁用LFS后推送...")
                    try:
                        # 先清理LFS引用
                        subprocess.run(['git', 'lfs', 'untrack', '*'], cwd=local_path, 
                                     capture_output=True, text=True, encoding='utf-8', errors='ignore')
                        # 移除.gitattributes中的LFS配置
                        gitattributes_path = os.path.join(local_path, '.gitattributes')
                        if os.path.exists(gitattributes_path):
                            try:
                                with open(gitattributes_path, 'r', encoding='utf-8') as f:
                                    content = f.read()
                                # 移除所有LFS相关行
                                lines = [line for line in content.split('\n') if 'filter=lfs' not in line and 'diff=lfs' not in line]
                                with open(gitattributes_path, 'w', encoding='utf-8') as f:
                                    f.write('\n'.join(lines))
                                self.logger.info("已清理.gitattributes中的LFS配置")
                            except Exception as attr_error:
                                self.logger.warning(f"清理.gitattributes失败: {attr_error}")
                        
                        # 使用git filter-branch清理Git历史中的LFS引用
                        self.logger.info("正在清理Git历史中的LFS引用...")
                        try:
                            # 创建临时脚本来清理LFS引用
                            filter_script = os.path.join(local_path, 'filter_lfs.sh')
                            with open(filter_script, 'w', encoding='utf-8') as f:
                                f.write('''#!/bin/bash
# 清理LFS引用的脚本
git filter-branch --force --index-filter '
    git rm --cached --ignore-unmatch -r . || true
    git add . || true
' --prune-empty --tag-name-filter cat -- --all
''')
                            
                            # 执行清理脚本
                            subprocess.run(['bash', filter_script], cwd=local_path, 
                                         capture_output=True, text=True, encoding='utf-8', errors='ignore')
                            self.logger.info("已清理Git历史中的LFS引用")
                            
                            # 清理临时文件
                            os.remove(filter_script)
                        except Exception as filter_error:
                            self.logger.warning(f"清理Git历史失败: {filter_error}")
                        
                        # 禁用LFS
                        subprocess.run(['git', 'lfs', 'uninstall'], cwd=local_path, 
                                     capture_output=True, text=True, encoding='utf-8', errors='ignore')
                        self.logger.info("已禁用LFS，继续推送...")
                    except Exception as lfs_error:
                        self.logger.warning(f"禁用LFS失败: {lfs_error}")
                    # 继续尝试普通推送
            
            # 推送所有分支和标签
            push_cmd = ['git', 'push', '--mirror', 'origin']
            result = subprocess.run(push_cmd, cwd=local_path, capture_output=True, text=True, encoding='utf-8', errors='ignore')
            
            if result.returncode != 0:
                # 如果推送失败，尝试不使用mirror模式
                self.logger.warning("镜像推送失败，尝试普通推送...")
                push_cmd_fallback = ['git', 'push', '--all', 'origin']
                result_fallback = subprocess.run(push_cmd_fallback, cwd=local_path, 
                                              capture_output=True, text=True, encoding='utf-8', errors='ignore')
                
                if result_fallback.returncode != 0:
                    self.logger.error(f"推送失败: {result_fallback.stderr}")
                    return False
                else:
                    self.logger.info(f"项目 {project_path} 推送成功（使用普通推送模式）")
                    return True
            else:
                self.logger.info(f"项目 {project_path} 推送成功")
                return True
            
        except Exception as e:
            self.logger.error(f"推送项目 {project_path} 时发生错误: {e}")
            return False
            
    def cleanup_temp_files(self, local_path: str):
        """清理临时文件"""
        try:
            if os.path.exists(local_path):
                # 尝试多次删除，因为Git可能还在使用某些文件
                import time
                max_retries = 3
                for attempt in range(max_retries):
                    try:
                        shutil.rmtree(local_path)
                        self.logger.info(f"临时目录已清理: {local_path}")
                        return
                    except PermissionError:
                        if attempt < max_retries - 1:
                            self.logger.info(f"清理临时目录失败，等待后重试... (尝试 {attempt + 1}/{max_retries})")
                            time.sleep(2)  # 等待2秒后重试
                        else:
                            raise
                self.logger.warning(f"清理临时目录失败，已达到最大重试次数: {local_path}")
        except Exception as e:
            self.logger.warning(f"清理临时目录失败: {e}")
            # 尝试强制删除（Windows特定）
            try:
                if os.name == 'nt':  # Windows
                    import subprocess
                    subprocess.run(['cmd', '/c', 'rmdir', '/s', '/q', local_path], 
                                 shell=True, capture_output=True, check=False)
                    self.logger.info(f"使用Windows命令强制清理临时目录: {local_path}")
            except Exception as force_error:
                self.logger.warning(f"强制清理临时目录也失败: {force_error}")
            
    def migrate_project(self, project_path: str) -> bool:
        """迁移单个项目"""
        self.logger.info(f"开始迁移项目: {project_path}")
        
        # 克隆源仓库
        local_path = self.clone_source_repo(project_path)
        if not local_path:
            return False
            
        try:
            # 设置目标远程仓库
            if not self.setup_target_remote(local_path, project_path):
                return False
                
            # 推送到目标仓库
            if not self.push_to_target(local_path, project_path):
                return False
                
            self.logger.info(f"项目 {project_path} 迁移成功")
            return True
            
        finally:
            # 清理临时文件
            self.cleanup_temp_files(local_path)
            
    def migrate_all_projects(self) -> bool:
        """迁移所有配置的项目"""
        if not self.validate_config():
            return False
            
        self.logger.info("开始Git仓库迁移")
        
        # 确保本地临时目录存在
        os.makedirs(self.config['local_temp_path'], exist_ok=True)
        
        success_count = 0
        total_count = len(self.config['source_project_paths'])
        
        for project_path in self.config['source_project_paths']:
            if self.migrate_project(project_path):
                success_count += 1
            else:
                self.logger.error(f"项目 {project_path} 迁移失败")
                
        self.logger.info(f"迁移完成: {success_count}/{total_count} 个项目成功")
        
        return success_count == total_count


def main():
    """主函数"""
    migrator = GitMigrator()
    
    if not migrator.config:
        print("配置文件不存在或格式错误，请先创建配置文件")
        return
        
    if migrator.migrate_all_projects():
        print("所有项目迁移成功！")
    else:
        print("部分项目迁移失败，请查看日志文件了解详情")


if __name__ == "__main__":
    main()

